Consider transitive fingerprints for freshness
authorAlex Crichton <alex@alexcrichton.com>
Thu, 29 Jan 2015 03:37:19 +0000 (19:37 -0800)
committerAlex Crichton <alex@alexcrichton.com>
Thu, 29 Jan 2015 06:01:35 +0000 (22:01 -0800)
Originally discovered through #1236, this commit fixes a bug in Cargo where
crates may not be recompiled when they need to (leading to obscure errors from
the compiler). The scenario in question looks like:

* Assume a dependency graph of `A -> B -> C` and `A -> C`
* Build all packages
* Modify C
* Rebuild, but hit Ctrl+C while B is building
* Modify A
* Rebuild again

Previously, Cargo only considered the freshness of a package to be the freshness
of the package itself (checking source files, for example). To handle transitive
recompilations, Cargo propagates a dirty bit throughout the dependency graph
automatically (instead if calculating it as such).

In the above example, however, we have a problem where as part of the last
rebuild Cargo thinks `B` and `C` are fresh! The artifact for `C` was just
recompiled, but `B`'s source code is untainted, so Cargo does not think that it
needs to recompile `B`. This is wrong, however, because one of `B`'s
dependencies was rebuilt, so it needs to be rebuilt.

To fix this problem, the fingerprint (a short hash) for all packages is now
transitively propagated (the fingerprint changes when an upstream package
changes). This should ensure that even when Ctrl+C is hit (or the situation
explained in #1236) that Cargo will still consider packages whose source code is
untainted as candidates for recompilation.

The implementation is somewhat tricky due to the actual fingerprint for a path
dependency not being known until *after* the crate is compiled (the fingerprint
is the mtime of the dep-info file).

Closes #1236

src/cargo/core/manifest.rs
src/cargo/core/package_id.rs
src/cargo/ops/cargo_rustc/context.rs
src/cargo/ops/cargo_rustc/fingerprint.rs
src/cargo/ops/cargo_rustc/mod.rs
tests/support/paths.rs
tests/test_cargo_cross_compile.rs
tests/test_cargo_freshness.rs
tests/test_cargo_test.rs

index 98365e8996251b789529dc954a572bb930a80f31..424eb5a35a2f841a68fda79fe2c15ddedbbd3c18 100644 (file)
@@ -68,7 +68,7 @@ impl Encodable for Manifest {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Hash, RustcEncodable, Copy)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, RustcEncodable, Copy)]
 pub enum LibKind {
     Lib,
     Rlib,
@@ -103,14 +103,14 @@ impl LibKind {
     }
 }
 
-#[derive(Debug, Clone, Hash, PartialEq, RustcEncodable)]
+#[derive(Debug, Clone, Hash, PartialEq, RustcEncodable, Eq)]
 pub enum TargetKind {
     Lib(Vec<LibKind>),
     Bin,
     Example,
 }
 
-#[derive(RustcEncodable, RustcDecodable, Clone, PartialEq, Debug)]
+#[derive(RustcEncodable, RustcDecodable, Clone, PartialEq, Eq, Debug)]
 pub struct Profile {
     env: String, // compile, test, dev, bench, etc.
     opt_level: u32,
@@ -345,7 +345,7 @@ impl<H: hash::Writer + hash::Hasher> hash::Hash<H> for Profile {
 }
 
 /// Informations about a binary, a library, an example, etc. that is part of the package.
-#[derive(Clone, Hash, PartialEq, Debug)]
+#[derive(Clone, Hash, PartialEq, Eq, Debug)]
 pub struct Target {
     kind: TargetKind,
     name: String,
@@ -467,7 +467,8 @@ impl Manifest {
 impl Target {
     pub fn file_stem(&self) -> String {
         match self.metadata {
-            Some(ref metadata) => format!("{}{}", self.name, metadata.extra_filename),
+            Some(ref metadata) => format!("{}{}", self.name,
+                                          metadata.extra_filename),
             None => self.name.clone()
         }
     }
index d82ce75a4d3a49c2cdde382099000cf9538bbd95..0e1825adbba66c6ee38f5d3a2eec34b779a410f0 100644 (file)
@@ -112,7 +112,7 @@ impl FromError<PackageIdError> for Box<CargoError> {
     fn from_error(t: PackageIdError) -> Box<CargoError> { Box::new(t) }
 }
 
-#[derive(PartialEq, Hash, Clone, RustcEncodable, Debug)]
+#[derive(PartialEq, Eq, Hash, Clone, RustcEncodable, Debug)]
 pub struct Metadata {
     pub metadata: String,
     pub extra_filename: String
index 81573d8b4d9f4af33bec2ccfd41ffb95e9b2dfaa..10a59c1bd92f2507ad068490f167ebe9190ef2a4 100644 (file)
@@ -5,14 +5,15 @@ use std::sync::Arc;
 
 use regex::Regex;
 
-use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target};
+use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target, Profile};
 use util::{self, CargoResult, ChainError, internal, Config, profile};
 use util::human;
 
-use super::{Kind, Compilation, BuildConfig};
 use super::TargetConfig;
-use super::layout::{Layout, LayoutProxy};
 use super::custom_build::BuildState;
+use super::fingerprint::Fingerprint;
+use super::layout::{Layout, LayoutProxy};
+use super::{Kind, Compilation, BuildConfig};
 use super::{ProcessEngine, ExecEngine};
 
 #[derive(Debug, Copy)]
@@ -29,6 +30,7 @@ pub struct Context<'a, 'b: 'a> {
     pub compilation: Compilation,
     pub build_state: Arc<BuildState>,
     pub exec_engine: Arc<Box<ExecEngine>>,
+    pub fingerprints: HashMap<(&'a PackageId, &'a Target, Kind), Fingerprint>,
 
     env: &'a str,
     host: Layout,
@@ -80,6 +82,7 @@ impl<'a, 'b: 'a> Context<'a, 'b> {
             build_state: Arc::new(BuildState::new(build_config.clone(), deps)),
             build_config: build_config,
             exec_engine: Arc::new(Box::new(ProcessEngine) as Box<ExecEngine>),
+            fingerprints: HashMap::new(),
         })
     }
 
@@ -357,6 +360,23 @@ impl<'a, 'b: 'a> Context<'a, 'b> {
     pub fn requested_target(&self) -> Option<&str> {
         self.build_config.requested_target.as_ref().map(|s| &s[])
     }
+
+    /// Calculate the actual profile to use for a target's compliation.
+    ///
+    /// This may involve overriding some options such as debug information,
+    /// rpath, opt level, etc.
+    pub fn profile(&self, target: &Target) -> Profile {
+        let mut profile = target.get_profile().clone();
+        let root_package = self.get_package(self.resolve.root());
+        for target in root_package.get_manifest().get_targets().iter() {
+            let root_profile = target.get_profile();
+            if root_profile.get_env() != profile.get_env() { continue }
+            profile = profile.opt_level(root_profile.get_opt_level())
+                             .debug(root_profile.get_debug())
+                             .rpath(root_profile.get_rpath())
+        }
+        profile
+    }
 }
 
 impl Platform {
index 95752de97aa2dd6eda02c2d1d32115d312a963b1..f37f35cada1421200e7748a8eb425fba3cc9affb 100644 (file)
@@ -1,5 +1,4 @@
 use std::collections::hash_map::Entry::{Occupied, Vacant};
-use std::hash::{Hash, Hasher, SipHasher};
 use std::io::{self, fs, File, BufferedReader};
 use std::io::fs::PathExtensions;
 
@@ -39,56 +38,28 @@ pub type Preparation = (Freshness, Work, Work);
 /// This function will calculate the fingerprint for a target and prepare the
 /// work necessary to either write the fingerprint or copy over all fresh files
 /// from the old directories to their new locations.
-pub fn prepare_target(cx: &mut Context, pkg: &Package, target: &Target,
-                      kind: Kind) -> CargoResult<Preparation> {
+pub fn prepare_target<'a, 'b>(cx: &mut Context<'a, 'b>,
+                              pkg: &'a Package,
+                              target: &'a Target,
+                              kind: Kind) -> CargoResult<Preparation> {
     let _p = profile::start(format!("fingerprint: {} / {:?}",
                                     pkg.get_package_id(), target));
     let new = dir(cx, pkg, kind);
     let loc = new.join(filename(target));
     cx.layout(pkg, kind).proxy().whitelist(&loc);
 
-    // We want to use the package fingerprint if we're either a doc target or a
-    // path source. If we're a git/registry source, then the mtime of files may
-    // fluctuate, but they won't change so long as the source itself remains
-    // constant (which is the responsibility of the source)
-    let use_pkg = {
-        let doc = target.get_profile().is_doc();
-        let path = pkg.get_summary().get_source_id().is_path();
-        doc || !path
-    };
-
     info!("fingerprint at: {}", loc.display());
 
-    // First bit of the freshness calculation, whether the dep-info file
-    // indicates that the target is fresh.
-    let dep_info = dep_info_loc(cx, pkg, target, kind);
-    let mut are_files_fresh = use_pkg ||
-                              try!(calculate_target_fresh(&dep_info));
-
-    // Second bit of the freshness calculation, whether rustc itself, the
-    // target are fresh, and the enabled set of features are all fresh.
-    let features = cx.resolve.features(pkg.get_package_id());
-    let features = features.map(|s| {
-        let mut v = s.iter().collect::<Vec<&String>>();
-        v.sort();
-        v
-    });
-    let rustc_fingerprint = if use_pkg {
-        mk_fingerprint(cx, &(target, try!(calculate_pkg_fingerprint(cx, pkg)),
-                             features))
-    } else {
-        mk_fingerprint(cx, &(target, features))
-    };
-    let is_rustc_fresh = try!(is_fresh(&loc, rustc_fingerprint.as_slice()));
+    let fingerprint = try!(calculate(cx, pkg, target, kind));
+    let is_fresh = try!(is_fresh(&loc, &fingerprint));
 
     let root = cx.out_dir(pkg, kind, target);
+    let mut missing_outputs = false;
     if !target.get_profile().is_doc() {
         for filename in try!(cx.target_filenames(target)).iter() {
             let dst = root.join(filename);
             cx.layout(pkg, kind).proxy().whitelist(&dst);
-            if are_files_fresh && !dst.exists() {
-                are_files_fresh = false;
-            }
+            missing_outputs |= !dst.exists();
 
             if target.get_profile().is_test() {
                 cx.compilation.tests.push((target.get_name().to_string(), dst));
@@ -104,7 +75,138 @@ pub fn prepare_target(cx: &mut Context, pkg: &Package, target: &Target,
         }
     }
 
-    Ok(prepare(is_rustc_fresh && are_files_fresh, loc, rustc_fingerprint))
+    Ok(prepare(is_fresh && !missing_outputs, loc, fingerprint))
+}
+
+/// A fingerprint can be considered to be a "short string" representing the
+/// state of a world for a package.
+///
+/// If a fingerprint ever changes, then the package itself needs to be
+/// recompiled. Inputs to the fingerprint include source code modifications,
+/// compiler flags, compiler version, etc. This structure is not simply a
+/// `String` due to the fact that some fingerprints cannot be calculated lazily.
+///
+/// Path sources, for example, use the mtime of the corresponding dep-info file
+/// as a fingerprint (all source files must be modified *before* this mtime).
+/// This dep-info file is not generated, however, until after the crate is
+/// compiled. As a result, this structure can be thought of as a fingerprint
+/// to-be. The actual value can be calculated via `resolve()`, but the operation
+/// may fail as some files may not have been generated.
+///
+/// Note that dependencies are taken into account for fingerprints because rustc
+/// requires that whenever an upstream crate is recompiled that all downstream
+/// dependants are also recompiled. This is typically tracked through
+/// `DependencyQueue`, but it also needs to be retained here because Cargo can
+/// be interrupted while executing, losing the state of the `DependencyQueue`
+/// graph.
+#[derive(Clone)]
+pub struct Fingerprint {
+    extra: String,
+    deps: Vec<Fingerprint>,
+    personal: Personal,
+}
+
+#[derive(Clone)]
+enum Personal {
+    Known(String),
+    Unknown(Path),
+}
+
+impl Fingerprint {
+    fn resolve(&self) -> CargoResult<String> {
+        let mut deps: Vec<_> = try!(self.deps.iter().map(|s| s.resolve()).collect());
+        deps.sort();
+        let known = match self.personal {
+            Personal::Known(ref s) => s.clone(),
+            Personal::Unknown(ref p) => {
+                debug!("resolving: {}", p.display());
+                try!(fs::stat(p)).modified.to_string()
+            }
+        };
+        debug!("inputs: {} {} {:?}", known, self.extra, deps);
+        Ok(util::short_hash(&(known, &self.extra, &deps)))
+    }
+}
+
+/// Calculates the fingerprint for a package/target pair.
+///
+/// This fingerprint is used by Cargo to learn about when information such as:
+///
+/// * A non-path package changes (changes version, changes revision, etc).
+/// * Any dependency changes
+/// * The compiler changes
+/// * The set of features a package is built with changes
+/// * The profile a target is compiled with changes (e.g. opt-level changes)
+///
+/// Information like file modification time is only calculated for path
+/// dependencies and is calculated in `calculate_target_fresh`.
+fn calculate<'a, 'b>(cx: &mut Context<'a, 'b>,
+                     pkg: &'a Package,
+                     target: &'a Target,
+                     kind: Kind)
+                     -> CargoResult<Fingerprint> {
+    let key = (pkg.get_package_id(), target, kind);
+    match cx.fingerprints.get(&key) {
+        Some(s) => return Ok(s.clone()),
+        None => {}
+    }
+
+    // First, calculate all statically known "salt data" such as the profile
+    // information (compiler flags), the compiler version, activated features,
+    // and target configuration.
+    let features = cx.resolve.features(pkg.get_package_id());
+    let features = features.map(|s| {
+        let mut v = s.iter().collect::<Vec<&String>>();
+        v.sort();
+        v
+    });
+    let extra = util::short_hash(&(cx.config.rustc_version(), target, &features,
+                                   cx.profile(target)));
+
+    // Next, recursively calculate the fingerprint for all of our dependencies.
+    let deps = try!(cx.dep_targets(pkg, target).into_iter().map(|(p, t)| {
+        let kind = match kind {
+            Kind::Host => Kind::Host,
+            Kind::Target if t.get_profile().is_for_host() => Kind::Host,
+            Kind::Target => Kind::Target,
+        };
+        calculate(cx, p, t, kind)
+    }).collect::<CargoResult<Vec<_>>>());
+
+    // And finally, calculate what our own personal fingerprint is
+    let personal = if use_dep_info(pkg, target) {
+        let dep_info = dep_info_loc(cx, pkg, target, kind);
+        match try!(calculate_target_mtime(&dep_info)) {
+            Some(i) => Personal::Known(i.to_string()),
+            None => {
+                // If the dep-info file does exist (but some other sources are
+                // newer than it), make sure to delete it so we don't pick up
+                // the old copy in resolve()
+                let _ = fs::unlink(&dep_info);
+                Personal::Unknown(dep_info)
+            }
+        }
+    } else {
+        Personal::Known(try!(calculate_pkg_fingerprint(cx, pkg)))
+    };
+    let fingerprint = Fingerprint {
+        extra: extra,
+        deps: deps,
+        personal: personal,
+    };
+    cx.fingerprints.insert(key, fingerprint.clone());
+    Ok(fingerprint)
+}
+
+
+// We want to use the mtime for files if we're a path source, but if we're a
+// git/registry source, then the mtime of files may fluctuate, but they won't
+// change so long as the source itself remains constant (which is the
+// responsibility of the source)
+fn use_dep_info(pkg: &Package, target: &Target) -> bool {
+    let doc = target.get_profile().is_doc();
+    let path = pkg.get_summary().get_source_id().is_path();
+    !doc && path
 }
 
 /// Prepare the necessary work for the fingerprint of a build command.
@@ -139,9 +241,13 @@ pub fn prepare_build_cmd(cx: &mut Context, pkg: &Package, kind: Kind,
     info!("fingerprint at: {}", loc.display());
 
     let new_fingerprint = try!(calculate_build_cmd_fingerprint(cx, pkg));
-    let new_fingerprint = mk_fingerprint(cx, &new_fingerprint);
+    let new_fingerprint = Fingerprint {
+        extra: String::new(),
+        deps: Vec::new(),
+        personal: Personal::Known(new_fingerprint),
+    };
 
-    let is_fresh = try!(is_fresh(&loc, new_fingerprint.as_slice()));
+    let is_fresh = try!(is_fresh(&loc, &new_fingerprint));
 
     // The new custom build command infrastructure handles its own output
     // directory as part of freshness.
@@ -178,9 +284,12 @@ pub fn prepare_init(cx: &mut Context, pkg: &Package, kind: Kind)
 
 /// Given the data to build and write a fingerprint, generate some Work
 /// instances to actually perform the necessary work.
-fn prepare(is_fresh: bool, loc: Path, fingerprint: String) -> Preparation {
-    let write_fingerprint = Work::new(move |desc_tx| {
-        drop(desc_tx);
+fn prepare(is_fresh: bool, loc: Path, fingerprint: Fingerprint) -> Preparation {
+    let write_fingerprint = Work::new(move |_| {
+        debug!("write fingerprint: {}", loc.display());
+        let fingerprint = try!(fingerprint.resolve().chain_error(|| {
+            internal("failed to resolve a pending fingerprint")
+        }));
         try!(File::create(&loc).write_str(fingerprint.as_slice()));
         Ok(())
     });
@@ -201,13 +310,17 @@ pub fn dep_info_loc(cx: &Context, pkg: &Package, target: &Target,
     return ret;
 }
 
-fn is_fresh(loc: &Path, new_fingerprint: &str) -> CargoResult<bool> {
+fn is_fresh(loc: &Path, new_fingerprint: &Fingerprint) -> CargoResult<bool> {
     let mut file = match File::open(loc) {
         Ok(file) => file,
         Err(..) => return Ok(false),
     };
 
     let old_fingerprint = try!(file.read_to_string());
+    let new_fingerprint = match new_fingerprint.resolve() {
+        Ok(s) => s,
+        Err(..) => return Ok(false),
+    };
 
     log!(5, "old fingerprint: {}", old_fingerprint);
     log!(5, "new fingerprint: {}", new_fingerprint);
@@ -215,17 +328,9 @@ fn is_fresh(loc: &Path, new_fingerprint: &str) -> CargoResult<bool> {
     Ok(old_fingerprint.as_slice() == new_fingerprint)
 }
 
-/// Frob in the necessary data from the context to generate the real
-/// fingerprint.
-fn mk_fingerprint<T: Hash<SipHasher>>(cx: &Context, data: &T) -> String {
-    let mut hasher = SipHasher::new_with_keys(0,0);
-    (cx.config.rustc_version(), data).hash(&mut hasher);
-    util::to_hex(hasher.finish())
-}
-
-fn calculate_target_fresh(dep_info: &Path) -> CargoResult<bool> {
+fn calculate_target_mtime(dep_info: &Path) -> CargoResult<Option<u64>> {
     macro_rules! fs_try {
-        ($e:expr) => (match $e { Ok(e) => e, Err(..) => return Ok(false) })
+        ($e:expr) => (match $e { Ok(e) => e, Err(..) => return Ok(None) })
     }
     let mut f = BufferedReader::new(fs_try!(File::open(dep_info)));
     // see comments in append_current_dir for where this cwd is manifested from.
@@ -233,7 +338,7 @@ fn calculate_target_fresh(dep_info: &Path) -> CargoResult<bool> {
     let cwd = Path::new(&cwd[..cwd.len()-1]);
     let line = match f.lines().next() {
         Some(Ok(line)) => line,
-        _ => return Ok(false),
+        _ => return Ok(None),
     };
     let line = line.as_slice();
     let mtime = try!(fs::stat(dep_info)).modified;
@@ -258,13 +363,13 @@ fn calculate_target_fresh(dep_info: &Path) -> CargoResult<bool> {
             Ok(stat) if stat.modified <= mtime => {}
             Ok(stat) => {
                 info!("stale: {} -- {} vs {}", file, stat.modified, mtime);
-                return Ok(false)
+                return Ok(None)
             }
-            _ => { info!("stale: {} -- missing", file); return Ok(false) }
+            _ => { info!("stale: {} -- missing", file); return Ok(None) }
         }
     }
 
-    Ok(true)
+    Ok(Some(mtime))
 }
 
 fn calculate_build_cmd_fingerprint(cx: &Context, pkg: &Package)
@@ -300,6 +405,7 @@ fn filename(target: &Target) -> String {
 // what that directory was at the beginning of the file so we can know about it
 // next time.
 pub fn append_current_dir(path: &Path, cwd: &Path) -> CargoResult<()> {
+    debug!("appending {} <- {}", path.display(), cwd.display());
     let mut f = try!(File::open_mode(path, io::Open, io::ReadWrite));
     let contents = try!(f.read_to_end());
     try!(f.seek(0, io::SeekSet));
index 20e72393a96ae29a81fe43d42f1c2880319d5e58..4b202c5202b0925b2c918fecbe7df121ea667dce 100644 (file)
@@ -594,15 +594,7 @@ fn build_base_args(cx: &Context,
 
     // Despite whatever this target's profile says, we need to configure it
     // based off the profile found in the root package's targets.
-    let mut profile = target.get_profile().clone();
-    let root_package = cx.get_package(cx.resolve.root());
-    for target in root_package.get_manifest().get_targets().iter() {
-        let root_profile = target.get_profile();
-        if root_profile.get_env() != profile.get_env() { continue }
-        profile = profile.opt_level(root_profile.get_opt_level())
-                         .debug(root_profile.get_debug())
-                         .rpath(root_profile.get_rpath())
-    }
+    let profile = cx.profile(target);
 
     let prefer_dynamic = profile.is_for_host() ||
                          (crate_types.contains(&"dylib") &&
index 837963837a6b6581d967c5c830c253d7cf6a99f3..3039ce9df4da6f02b947cc2da3451adcee279898 100644 (file)
@@ -61,7 +61,9 @@ impl PathExt for Path {
         if self.is_file() {
             try!(time_travel(self));
         } else {
+            let target = self.join("target");
             for f in try!(fs::walk_dir(self)) {
+                if target.is_ancestor_of(&f) { continue }
                 if !f.is_file() { continue }
                 try!(time_travel(&f));
             }
index a4d2ed024e7c9640572d8ce62f2604327bd19f6a..3337e7d91fa197d281e75d9d61906baa0ec767c5 100644 (file)
@@ -247,14 +247,15 @@ test!(plugin_to_the_max {
             version = "0.0.1"
             authors = []
         "#)
-        .file("src/lib.rs", "pub fn baz() -> int { 1 }");
+        .file("src/lib.rs", "pub fn baz() -> i32 { 1 }");
     bar.build();
     baz.build();
 
     let target = alternate();
-    assert_that(foo.cargo_process("build").arg("--target").arg(target),
+    assert_that(foo.cargo_process("build").arg("--target").arg(target).arg("-v"),
                 execs().with_status(0));
-    assert_that(foo.process(cargo_dir().join("cargo")).arg("build")
+    println!("second");
+    assert_that(foo.process(cargo_dir().join("cargo")).arg("build").arg("-v")
                    .arg("--target").arg(target),
                 execs().with_status(0));
     assert_that(&foo.target_bin(target, "foo"), existing_file());
index 699da047e9b573c4bf0f521fa797abe3ef2fe1cd..03107be6cdb0b289e79fb1dae1083b1023ee13da 100644 (file)
@@ -83,3 +83,54 @@ test!(modify_only_some_files {
 ", compiling = COMPILING, dir = path2url(p.root()))));
     assert_that(&p.bin("foo"), existing_file());
 });
+
+test!(rebuild_sub_package_then_while_package {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            authors = []
+            version = "0.0.1"
+
+            [dependencies.a]
+            path = "a"
+            [dependencies.b]
+            path = "b"
+        "#)
+        .file("src/lib.rs", "extern crate a; extern crate b;")
+        .file("a/Cargo.toml", r#"
+            [package]
+            name = "a"
+            authors = []
+            version = "0.0.1"
+            [dependencies.b]
+            path = "../b"
+        "#)
+        .file("a/src/lib.rs", "extern crate b;")
+        .file("b/Cargo.toml", r#"
+            [package]
+            name = "b"
+            authors = []
+            version = "0.0.1"
+        "#)
+        .file("b/src/lib.rs", "");
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0));
+
+    File::create(&p.root().join("b/src/lib.rs")).unwrap().write_str(r#"
+        pub fn b() {}
+    "#).unwrap();
+
+    assert_that(p.process(cargo_dir().join("cargo")).arg("build").arg("-pb"),
+                execs().with_status(0));
+
+    File::create(&p.root().join("src/lib.rs")).unwrap().write_str(r#"
+        extern crate a;
+        extern crate b;
+        pub fn toplevel() {}
+    "#).unwrap();
+
+    assert_that(p.process(cargo_dir().join("cargo")).arg("build"),
+                execs().with_status(0));
+});
index e9e833a22eb5fc687941e8de11bfb9e34adabacc..032154a162a01418baa9c22fc8420959a27b5a96 100644 (file)
@@ -1020,7 +1020,6 @@ test!(selective_testing {
     assert_that(p.process(cargo_dir().join("cargo")).arg("test")
                  .arg("-p").arg("d1"),
                 execs().with_status(0)
-                       .with_stderr("")
                        .with_stdout(format!("\
 {compiling} d1 v0.0.1 ({dir})
 {running} target[..]d1-[..]
@@ -1035,7 +1034,6 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n
     assert_that(p.process(cargo_dir().join("cargo")).arg("test")
                  .arg("-p").arg("d2"),
                 execs().with_status(0)
-                       .with_stderr("")
                        .with_stdout(format!("\
 {compiling} d2 v0.0.1 ({dir})
 {running} target[..]d2-[..]
@@ -1049,7 +1047,6 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n
     println!("whole");
     assert_that(p.process(cargo_dir().join("cargo")).arg("test"),
                 execs().with_status(0)
-                       .with_stderr("")
                        .with_stdout(format!("\
 {compiling} foo v0.0.1 ({dir})
 {running} target[..]foo-[..]